Skip to content

chore(ci): migrate npm publish to trusted publishing via OIDC#1609

Merged
joshuayoes merged 2 commits into
masterfrom
chore/npm-oidc-publish
May 1, 2026
Merged

chore(ci): migrate npm publish to trusted publishing via OIDC#1609
joshuayoes merged 2 commits into
masterfrom
chore/npm-oidc-publish

Conversation

@joshuayoes
Copy link
Copy Markdown
Contributor

@joshuayoes joshuayoes commented May 1, 2026

Please verify the following:

  • yarn build-and-test:local passes
  • I have added tests for any new features, if relevant
  • README.md (or relevant documentation) has been updated with your changes

Describe your PR

Migrates the reactotron CI publish pipeline from classic NPM_TOKEN auth to npm Trusted Publishing via CircleCI OIDC. npm GA'd CircleCI support on 2026-04-06; this PR wires us up.

The pipeline has been broken since npm revoked classic tokens on 2025-12-09 and #1602 left the renamed reactotron-npm-context with an empty NPM_TOKEN. Rather than mint a granular replacement (capped at 90 days) and rotate forever, OIDC eliminates the human-managed token entirely.

Changes

  • .circleci/config.yml (release_package job) — replaces the npm whoami + ~/.npmrc token write with a "Mint npm OIDC token" step using circleci run oidc get --claims '{"aud":"npm:registry.npmjs.org"}'.
  • scripts/release.artifacts.mjs — accepts either NPM_TOKEN or NPM_ID_TOKEN, and performs the npm OIDC token exchange directly (POST /-/npm/v1/oidc/token/exchange/package/<ident>). This in-script exchange is a workaround for yarnpkg/berry#7122: Yarn 4.14.1's getOidcToken helper handles CircleCI, but the allowOidc gate in publish.ts only flips on for GITHUB_ACTIONS / GITLAB_CI. Once 7122 lands and we bump Yarn, the script-level exchange block can be deleted.
  • Yarn 4.1.1 → 4.14.1 (4.14 brought the CircleCI OIDC support that 7122 finishes wiring up).
  • .yarnrc.yml — keeps npmAuthToken: "${NPM_TOKEN-}" as a soft fallback during cutover. Empty string is falsy in Yarn's auth chain, so it's a no-op when OIDC is in play. Removed in a follow-up PR after first prod publish.
  • docs/contributing/releasing.md — new "OIDC publish flow" section covering the CI flow, how to add a trusted publisher when shipping a new package, and the upstream Yarn workaround.

Pilot validation

Pilot package: eslint-plugin-reactotron@0.1.10-beta.0. Pushed tag from branch test/oidc-pilot-eslint, published under the beta dist-tag (semver prerelease — invisible to ^/~/* ranges). Pipeline succeeded; package published.

The npm registry metadata for the pilot version records:

"_npmUser": {
  "name": "CircleCI",
  "email": "npm-oidc-no-reply@github.com",
  "trustedPublisher": {
    "id": "circleci",
    "oidcConfigId": "oidc:71dbce82-a25e-4b17-a0e3-78908cc0e805"
  }
}

This is the smoking-gun proof the publish ran via the trusted publisher path, not legacy token auth. _npmVersion: null and _nodeVersion: null (set when the publisher is OIDC) corroborate.

Pre-merge prerequisites

  • ✅ Trusted publisher configured on all 11 npm packages (same Org / Project / Pipeline Definition / Context / VCS Origin tuple, validated by the pilot's successful publish).
  • ✅ "Publishing access" tightened to "Require two-factor authentication and disallow tokens (recommended)" on all 11 packages. Per npm's inline note, this is fully OIDC-compatible — short-lived OIDC publish tokens are a separate auth path.

Out of scope: reactotron-app (Electron app, GitHub release artifacts only), reactotron-mcp ("private": true).

Follow-up PRs

After first successful production OIDC publish:

  • Remove npmAuthToken: "${NPM_TOKEN-}" from .yarnrc.yml.
  • Tighten release.artifacts.mjs to require only NPM_ID_TOKEN.
  • Clear NPM_TOKEN from CircleCI context (currently absent; belt-and-suspenders).

After yarnpkg/berry#7122 lands and we bump Yarn:

  • Delete the OIDC exchange block in scripts/release.artifacts.mjs — Yarn handles the exchange directly via yarn npm publish.
  • Simplify the OIDC docs section.

Notes

  • The chore commit only touches infra files (.circleci/, .yarnrc.yml, root package.json, yarn.lock, scripts/release.artifacts.mjs, docs/, .yarn/releases/). No lib/* or apps/* workspaces affected → 0 release tags created on merge → no auto-publish triggered. First real OIDC publish happens on the next legitimate code change inside a lib/* workspace.

🤖 Generated with Claude Code

joshuayoes and others added 2 commits April 28, 2026 12:45
The reactotron CI publish pipeline has been broken since npm revoked
classic tokens on 2025-12-09 and the prior fix-attempt (PR #1602) left
the renamed CircleCI context with an empty NPM_TOKEN. Rather than
regenerate a granular token (capped at 90 days) and rotate forever,
move publishing to npm Trusted Publishing via CircleCI OIDC.

Changes:

- Bump Yarn 4.1.1 → 4.14.1 (yarn 4.14 added CircleCI OIDC support).
- .circleci/config.yml release_package job now mints a short-lived OIDC
  id-token via `circleci run oidc get --claims '{"aud":"npm:..."}'` and
  exports it as NPM_ID_TOKEN.
- scripts/release.artifacts.mjs accepts either NPM_TOKEN or
  NPM_ID_TOKEN, then performs the npm OIDC token exchange directly
  (POST /-/npm/v1/oidc/token/exchange/package/<name>) and surfaces the
  result as NPM_TOKEN. The exchange is done in the script rather than
  by yarn because yarn 4.14.1's `yarn npm publish` skips the OIDC code
  path on CircleCI even though its `getOidcToken` helper supports it
  (gating bug — fix submitted upstream as yarnpkg/berry#7122).
- .yarnrc.yml retains `npmAuthToken: "${NPM_TOKEN-}"` as a cutover
  soft-fallback (empty string is falsy in yarn's auth chain, so it
  doesn't block OIDC).
- docs/contributing/releasing.md gains an OIDC section covering the CI
  flow, how to add a trusted publisher when shipping a new package, and
  the upstream yarn bug.

Once yarnpkg/berry#7122 ships and we bump yarn, the OIDC exchange block
in release.artifacts.mjs can be deleted and `yarn npm publish` will
consume NPM_ID_TOKEN directly.

Before merging this PR, the 11 published packages need a CircleCI
trusted publisher configured on npmjs.com (org/project/context IDs from
the CircleCI project settings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous wording called the unrelated reactotron-mcp package "squatted",
which mischaracterizes it. It's steve228uk's own project; just unrelated
to Infinite Red. Reword to a neutral attribution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joshuayoes joshuayoes merged commit e17aa58 into master May 1, 2026
7 checks passed
@joshuayoes joshuayoes deleted the chore/npm-oidc-publish branch May 1, 2026 19:07
joshuayoes added a commit that referenced this pull request May 4, 2026
## Please verify the following:

- [x] `yarn build-and-test:local` passes
- [ ] I have added tests for any new features, if relevant
- [x] `README.md` (or relevant documentation) has been updated with your
changes

## Describe your PR

Bumps the `infinitered/publish-docs` orb from `@0.4` (resolves to
v0.4.13) to `@0.5` (resolves to v0.5.1). This unbreaks
`publish-docs/publish_docs` on master pushes when the merge commit body
contains markdown / multi-line content.

### Why

The orb's v0.4.13 wrote unescaped multi-line shell content to
`$BASH_ENV` via `echo "export VAR=\"$VAL\""`. When the merge commit's
body had colons (e.g. inline JSON snippets) or newlines, bash
interpreted each line as a command after sourcing `$BASH_ENV` at the
start of the next step. Symptom on the master push for #1609:

```
/tmp/.bash_env-...-build: line 39: README.md: command not found
/tmp/.bash_env-...-build: line 39: NPM_TOKEN: command not found
... [40+ more]
Error: Not a GitHub URL.
Exited with code exit status 1
```

### Changes

- `.circleci/config.yml` — bump `publish-docs:
infinitered/publish-docs@0.4` → `@0.5`.

### Notes

- Fix is upstream in
[infinitered/orb-publish-docs#40](infinitered/orb-publish-docs#40),
released as `v0.5.1` today. Replaces the unsafe `echo "export
VAR=\"$VAL\""` patterns with `printf 'export VAR=%q\n' "$VAL"` in 5
internal scripts.
- The `0.4 → 0.5` diff is internal only — no job, command, or parameter
signatures change. All existing usage (`publish-docs/build_docs` and
`publish-docs/publish_docs` with `ir_docs_config` params) remains
compatible.
- Verified `npx nx affected --target=version --base=origin/master
--head=HEAD` returns 0 projects, so this merge will not cascade-trigger
any unintended package releases.
- Using `@0.5` (auto-resolves to latest 0.5.x) rather than pinned
`@0.5.1` so future patch fixes apply automatically. Matches the prior
`@0.4` style.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant